به نام خدا


تحلیل داده‌های متن


مرتب‌سازی داده


چرا باید داده را مرتب کنیم؟


هنگامی که با تحلیل داده سر و کار داریم، اکثر مواقع داده‌ی در دست، داده‌ای نامرتب و به هم‌ریخته است. در قدم اول کاری که تحلیل را آسان‌تر و کاراتر می‌کند تمیز کردن و ساختار دادن به داده است. در کار با داده‌های متنی نیز ابتدا باید آن‌ را به نحوی مرتب کنیم که نیازهای ما را برآورده کند.

این مقاله داده‌ی مرتب را داده‌ای با ویژگی‌های زیر تعریف می‌کند:
  1. هر متغیر یک ستون است.
  2. هر مشاهده یک سطر است.
  3. هر مدل از واحد مشاهده یک جدول است.

برای داده‌های متنی، مدل تمیز شده را جدولی با یک token در هر سطر تعریف می‌کنیم. یک token، واحد معناداری از متن است (مثلا یک کلمه یا یک جمعه) که ما برای تحلیل از آن استفاده می‌کنیم. در مرتب‌سازی داده‌های متنی، تلاش می‌کنیم که طی فرآیند tokenization متن را به tokenها تقسیم کنیم.

پس از این مقدمه و تعاریف، پیاده‌سازی این عملیات در R را شروع می‌کنیم.


نصب کتاب‌خانه‌ی tidytext


با استفاده از یکی از دو روش زیر، کتابخانه‌ی tidytext را نصب کنید.
In [1]:
# روش اول
install.packages("tidytext")

# روش دوم
library(devtools)
install_github("juliasilge/tidytext")
Installing package into ‘/usr/local/lib/R/site-library’
(as ‘lib’ is unspecified)
Warning message in install.packages("tidytext"):
“'lib = "/usr/local/lib/R/site-library"' is not writable”
Error in install.packages("tidytext"): unable to install packages
Traceback:

1. install.packages("tidytext")
2. stop("unable to install packages")
پس از اتمام فرآیند نصب، با اجرای دستور زیر می‌توانید از آن استفاده کنید.
In [2]:
library(tidytext)


تابع unnest_tokens


داده‌ی ساده‌ي زیر شامل چند بیت آغاز شاهنامه‌ی فردوسی است.
In [3]:
library(tidyverse)
df <- data_frame(text = c("به نام خداوند جان و خرد", 
       "کزین برتر اندیشه برنگذرد",
       "خداوند نام و خداوند جای",
       "خداوند روزی ده رهنمای"))
── Attaching packages ─────────────────────────────────────── tidyverse 1.2.1 ──
✔ ggplot2 3.1.0     ✔ purrr   0.2.5
✔ tibble  2.0.1     ✔ dplyr   0.7.8
✔ tidyr   0.8.2     ✔ stringr 1.3.1
✔ readr   1.3.1     ✔ forcats 0.3.0
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
Warning message:
“`data_frame()` is deprecated, use `tibble()`.
This warning is displayed once per session.”
می‌خواهیم این دیتافریم را طبق تعریفی که قبلا ارائه کردیم tokenize کنیم. تابع $\texttt{unnest_tokens()}$ به شکل بسیار راحتی این کار را برای ما می‌کند. این تابع ورودی‌های زیر را می‌گیرد:
  1. دیتافریمی که باید tokenize کند.
  2. نام ستون خروجی شامل tokenها
  3. نام ستون ورودی شامل متن
  4. نوع token
تا اینجا مورد اول تا سوم برای ما کافی هستند، در رابطه با مورد چهارم جلوتر توضیح خواهیم داد. مطابق با توضیحات بالا، تابع را فراخوانی می‌کنیم
In [4]:
df %>% unnest_tokens(word, text)
word
به
نام
خداوند
جان
و
خرد
کزین
برتر
اندیشه
برنگذرد
خداوند
نام
و
خداوند
جای
خداوند
روزی
ده
رهنمای
می‌بینید که در ستون خروجی word، کلمات جداشده‌ی متن قرار دارند.


یک قدم فراتر: بررسی چند بخش شاهنامه


داده‌ي موجود در فایل sample.txt شامل ۱۰ بخش اول داستان رستم و اسفندیار شاهنامه است. با دستور زیر داده را می‌خوانیم.
In [5]:
rostam_esfandiar <- read.delim(file = "sample.txt", stringsAsFactors = F, header = F)
colnames(rostam_esfandiar) = "beyt"
پس از بررسی داده، متوجه می‌شویم که شروع هر بخش با «*بخش n*» مشخص شده است که n عدد با فونت فارسی است. می‌خواهیم بخش‌های مختلف کتاب را از هم جدا کنیم. به regex زیر توجه کنید:
In [6]:
regex_chapter <- "بخش [۰۱۲۳۴۵۶۷۸۹]+"
این regex تشخیص می‌دهد که «بخش n» در متن قرار دارد یا نه. برای اطلاعات بیشتر از regexها به اینجا و اینجا مراجعه کنید.

با استفاده از دستورات زیر مشخص می‌کنیم که هر بیت مربوط به کدام بخش است:
In [7]:
library(stringr)
rostam_esfandiar <- rostam_esfandiar %>%
    mutate(chapter = cumsum(str_detect(beyt, regex_chapter)))
rostam_esfandiar %>% sample_n(10)
beytchapter
841چو از من گناهی بیابد پدید 10
885عنان با عنان تو بندم به راه 10
92همی تاج و تخت آرزو آیدش 2
707همان به که با او مدارا کند 9
198به یزدان نمایم به روز شمار 3
777ازان خوردن و یال و بازوی و کفت 9
80چو گویی سخن بازیابی بکوی 2
12به پالیز بلبل بنالد همی 1
615که آمد نبرده سواری دلیر 8
102نگه کرد آن زیجهای کهن 2
مانند فرآیندی که در بخش قبل انجام دادیم، کلمات را جدا کرده، تعداد آن ها در هر فصل را بدست می‌آوریم و به ترتیب نزولی می‌چینیم.
In [8]:
rostam_esfandiar <- rostam_esfandiar %>% 
    unnest_tokens(word, beyt) %>% 
    group_by(chapter, word) %>% 
    summarise(count = n()) %>% 
    arrange(chapter, desc(count))
rostam_esfandiar %>% head(10)
chapterwordcount
1 همی 10
1 از 7
1 و 7
1 به 6
1 که 6
1 بلبل 5
1 گل 5
1 ابر 4
1 چو 4
1 باد 3
در هر بخش ۱۰ کلمه‌ی پر‌تکرار را انتخاب می‌کنیم
In [9]:
rostam_esfandiar.top10 <- rostam_esfandiar %>% 
  mutate(rank = rank(-count) %>% as.integer(), 
         rank = row_number(rank)) %>% 
  filter(rank <= 10)
در نهایت نمودار فراوانی این کلمات را رسم می‌کنیم
In [10]:
p <- ggplot(rostam_esfandiar.top10, aes(x = reorder(word, -count), y = count, fill = as.factor(paste("بخش", chapter, sep = " ")))) + 
  geom_bar(stat = "identity", show.legend = FALSE) + 
  facet_wrap(~chapter, ncol = 5,  scales = "free") + 
  theme_minimal() + 
  theme(axis.title.x = element_blank(), axis.title.y = element_blank()) + 
  coord_flip()
چون ggplot برای زبان‌های راست به چپ درست عمل نمی‌کند از کتابخانه‌ی plotly استفاده می‌کنیم.
In [13]:
library(plotly)
ggplotly(p)


تحلیل متن با استفاده از فراوانی


تعریف معیار tf-idf


اولین معیاری که برای میزان مهم بودن یک کلمه به ذهن می‌رسد این است که تعداد دفعات استفاده از آن را در نظر بگیریم. ولی در قسمت قبل دیدیم که با این کار تعداد زیادی کلمات بی‌معنی به عنوان کلمات مهم انتخاب می‌شوند. شاید وسوسه شویم که لیستی از کلمات اضافه و بی‌معنی تعریف کنیم و این کلمات را از متن حذف کنیم. ولی این کار، حرفه‌ای نیست و لزوما نتایج درستی به همراه نخواهد داشت.

فرض کنید میخواهیم کلمات مهم هر فصل‌ شاهنامه را استخراج کنیم. در واقع ما به دنبال کلماتی هستیم که علاوه بر اینکه در یک فصل زیاد تکرار شده‌اند در فصول دیگر زیاد استفاده نشده باشد. بدین منظور دو معیار زیر را تعریف می‌کنیم:
  • معیار $\text{TF(Term Frequency)}$: تعداد دفعات تکرار این کلمه در مستند به تعداد کل کلمات مستند

    $$tf(term) = \frac{n_{term~in~document}}{n_{total~ document~words}}$$


  • معیار $\text{IDF(Inverse Document Frequency)}$: لگاریتم طبیعی معکوس تعداد مستندات شامل این کلمه به تعداد کل مستندات. $$idf(term) = ln\Big(\frac{n_{documents}}{n_{documents~containing~term}}\Big)$$

معیار tf-idf را ضرب این دو عدد تعریف می‌کنیم.


پیاده‌سازی tf-idf در R


تابع $\texttt{bind_tf_idf}$ در کتابخانه‌ی tidytext این معیار‌ها را به سادگی محاسبه می‌کند. این تابع ورودی‌های زیر را می‌گیرد:
  1. دیتافریمی که باید بررسی کند.
  2. نام ستون ورودی شامل tokenها
  3. نام ستون ورودی شامل نام مستندات
  4. نام ستون ورودی شامل تعداد دفعاتی که هر token در مستند مربوطه تکرار شده‌است.
می‌خواهیم عملکرد این تابع را بررسی کنیم. دیتافریم زیر، شامل ۱۰ بخش اول داستان‌های رستم و اسفندیار، سهراب، ضحاک، سیاوس و پادشاهی بهرام گور در شاهنامه‌ي فردوسی است.
In [15]:
shahname <- read_csv("shahname.csv")
shahname %>% head(5)
Parsed with column specification:
cols(
  text = col_character(),
  book = col_character()
)
textbook
*بخش ۱* bahram
چو بر تخت بنشست بهرام گور bahram
برو آفرین کرد بهرام و هور bahram
پرستش گرفت آفریننده را bahram
جهاندار و بیدار و بیننده راbahram
مطابق بخش اول، متن را tokenize می‌کنیم، تعداد دفعات تکرار هر کلمه در هرکتاب را بدست می‌آوریم. می‌بینید که کلماتی که از همه بیشتر تکرار شده‌اند بی‌معنی‌اند.
In [16]:
shahname.words <- shahname %>% 
    unnest_tokens(word, text) %>%
    group_by(book, word) %>% 
    summarise(count = n())
shahname.words %>% arrange(book, desc(count)) %>% head(5)
bookwordcount
bahramو 288
bahramبه 235
bahramکه 130
bahramز 98
bahramاز 96
حال از تابع $\texttt{bind_tf_idf}$ استفاده می‌کنیم. می‌بینید که کلمات با tf-idf بالاتر از لحاظ منطقی بامعنا‌ترند.
In [ ]:
shahname.words <- shahname.words %>% 
    bind_tf_idf(word, book, count) %>% 
    arrange(book, desc(tf_idf))
shahname.words %>% head(10)
در نهایت نمودار ۱۰ کلمات کلیدی هر فصل را رسم می‌کنیم.
In [ ]:
plot.data <- shahname.words %>% 
  mutate(rank = rank(-tf_idf) %>% as.integer(), 
         rank = row_number(rank)) %>% 
  filter(rank <= 10) %>% arrange(book, rank)
In [ ]:
p <- ggplot(plot.data, aes(x = reorder(word, -tf_idf), y = tf_idf, fill = book)) + 
  geom_bar(stat = "identity", show.legend = FALSE) + 
  facet_wrap(~book, ncol = 2,  scales = "free") + 
  theme_minimal()
In [ ]:
ggplotly(p)


تحلیل متن با استفاده از رابطه‌ی بین کلمات


تعریف n-گرام


تا به حال کل کاری که برای تحلیل متن کردیم بررسی تک‌ تک کلمات یا رابطه‌شان با کل مستند بود. در حالی که برای تحلیل کامل و جامع تنها این کافی نیست. واضح است که کلمات بین خودشان نیز با هم رابطه دارند.
واژه‌ي n-گرام به معنای کلمات nتایی متوالی موجود در متن است. برای مثال به مصراع زیر توجه کنید:
بسی رنج بردم در این سال سی
عبارات «بسی رنج بردم»، «رنج بردم در»، «بردم در این»، «در این سال» و «این سال سی» ۳-گرام‌های آن هستند.


بدست آوردن n-گرام‌ها در R


حال نوبت آن رسیده است که ورودی چهارم تابع $\texttt{unnest_tokens()}$، یعنی نوع tokenها را مورد بررسی قرار دهیم.

برای اینکه بتوانیم n-گرام‌ها را با استفاده از این تابع بدست آوریم، باید علاوه بر ورودی‌های قبلی، گزینه‌ی$\texttt{token = “ngrams”}$ را نیز به آن اضافه کنیم. هم‌چنین ورودی n در این تابع، بیانگر طول n-گرام‌هاست.
برای مثال سعی می‌کنیم ۲-گرام‌های متون شاهنامه‌ که در قسمت قبل نیز استفاده کردیم را بدست آوریم:
In [ ]:
shahname.bigrams <- shahname %>% 
    unnest_tokens(bigram, text, token = "ngrams", n = 2)

shahname.bigrams %>% head(5)
مانند قبل آن ها را بر حسب تعداد دفعات تکرار در هر فصل مرتب می‌کنیم:
In [ ]:
shahname.bigrams <- shahname.bigrams %>% 
    group_by(book, bigram) %>% 
    summarise(count = n()) %>% 
    arrange(book, desc(count)) 

shahname.bigrams %>% arrange(book, desc(count)) %>%head(5)
برخلاف تک‌کلمات که خیلی بی‌معنی بودند، این کلمات نسبتا با معنی‌ترند.

با استفاده از تابع $\texttt{separate موجود در کتابخانه‌ی tidyr می‌توان این دو کلمه را از هم جدا کرد.
In [ ]:
#3
shahname.bigrams %>% 
    separate(bigram, c("word1", "word2"), sep = " ") %>% 
    head(5)
در صورتی که از این تابع استفاده کردید و پس از آن خواستید مجدد این دو کلمه را به هم بچسبانید می‌توانید، از تابع $\texttt{unite()}$ استفاده کنید.


تحلیل n-گرام‌ها با استفاده از tf-idf


همانند تک‌کلمات که قبلا با tf-idf تحلیل کردیم، n-گرام‌ها را نیز می‌توان با این روش تحلیل کرد.
In [ ]:
shahname.bigrams <- shahname.bigrams %>% 
    bind_tf_idf(bigram, book, count) %>% 
    arrange(book, desc(tf_idf))

shahname.bigrams %>% head(5)
نمودار ۱۰تای برتر را رسم می‌کنیم.
In [ ]:
plot.data <- shahname.bigrams %>% 
    mutate(rank = rank(-tf_idf) %>% as.integer(), rank = row_number(rank)) %>% 
    filter(rank <= 10) %>% 
    arrange(book, rank)
In [ ]:
p <- ggplot(plot.data, aes(x = reorder(bigram, -tf_idf), y = tf_idf, fill = book)) + 
  geom_bar(stat = "identity", show.legend = FALSE) + 
  facet_wrap(~book,scales = "free") + 
  theme_minimal() + 
  theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1), 
        axis.title.x = element_blank(), axis.title.y = element_blank())
In [ ]:
ggplotly(p)
In [ ]: